package sorm

import (
	"database/sql"
	"gitee.com/zhendliu/sorm/dialect"
	"gitee.com/zhendliu/sorm/log"
	"gitee.com/zhendliu/sorm/operator"
	"gitee.com/zhendliu/sorm/server"
	"gitee.com/zhendliu/sorm/session"
	"gitee.com/zhendliu/sorm/spliter"
)

// Engine is the main struct of sorm, manages all db sessions and transactions.
type Engine struct {
	db       *server.DB
	spliter  map[string]spliter.Split // 数据库分表的信息 key为表名 value为分表的信息
	dbs      map[string]*server.DB
	operator operator.Operator
	config   *server.Config
}

// NewEngine create a instance of Engine
// connect database and ping it to test whether it's alive
func NewEngine(driver, source string) (e *Engine, err error) {
	db, err := sql.Open(driver, source)
	if err != nil {
		log.Error(err)
		return
	}
	// Send a ping to make sure the database connection is alive.
	if err = db.Ping(); err != nil {
		log.Error(err)
		return
	}

	dia, _ := dialect.NewDialect("sqlite3")
	e = &Engine{db: &server.DB{Db: db, Dia: dia}, spliter: map[string]spliter.Split{}} // 初始化一个空的spliter
	log.Info("Connect database success")
	return
}

// 通过config 初始化一个engine
func NewEngineWithConfig(config *server.Config) (e *Engine, err error) {
	sdb, err := server.NewDBWithConfig(config)
	if err != nil {
		log.Error(err)
		return
	}
	e = &Engine{config: config, db: sdb, spliter: map[string]spliter.Split{}, operator: operator.NewOpt()} // 初始化一个空的spliter
	return
}

// 给engine 中 通过tableName 和config 增加dbs 用于分库
// tableName 分表的原来表名
// shardType 分表的类型  例如  %2 对应的表名为  user_0/user_1  m 对应的表名为 user_202001 d 对应的表名为 user_20200101 w 对应的表名为 user_20200101 h 对应的表名为 user_2020010101 y 对应的表名为 user_2020
// segName 分表的字段
func (e *Engine) AddDBWithShard(tableName, shardType, segName string, config *server.Config) {
	if e.dbs == nil {
		e.dbs = map[string]*server.DB{}
	}
	sdb, err := server.NewDBWithConfig(config)
	if err != nil {
		log.Error(err)
		return
	}
	e.dbs[tableName] = sdb
	// %1 就是不分表
	if shardType != "%1" {
		e.AddSpliter(spliter.Split{TableName: tableName, TableShardType: shardType, TableSegName: segName})
	}
}

func (e *Engine) AddDB(tableName string, config *server.Config) {
	if e.dbs == nil {
		e.dbs = map[string]*server.DB{}
	}
	sdb, err := server.NewDBWithConfig(config)
	if err != nil {
		log.Error(err)
		return
	}
	e.dbs[tableName] = sdb
}

// 给engine 新增一个spliter
func (engine *Engine) AddSpliter(split spliter.Split) {
	if engine.spliter == nil {
		engine.spliter = map[string]spliter.Split{}
	}
	(engine.spliter)[split.TableName] = split
}

// 给engine 新增多个spliter
func (engine *Engine) AddSpliterMap(splitMap map[string]spliter.Split) {
	if engine.spliter == nil {
		engine.spliter = map[string]spliter.Split{}
	}
	for k, v := range splitMap {
		(engine.spliter)[k] = v
	}
}

// 生成一个 模拟user 按照%4 的拆分表规则的spliter
func (engine *Engine) AddUserSplitter() {
	if engine.spliter == nil {
		engine.spliter = map[string]spliter.Split{}
	}
	(engine.spliter)["user"] = spliter.Split{
		TableShardType: "m",
		TableSegName:   "uer_id",
	}
	(engine.spliter)["person"] = spliter.Split{
		TableShardType: "%5",
		TableSegName:   "age",
	}

	(engine.spliter)["point"] = spliter.Split{
		TableShardType: "%5",
		TableSegName:   "num",
	}
}

// Close database connection
func (engine *Engine) Close() {
	if err := engine.db.Db.Close(); err != nil {
		log.Error("Failed to close database")
		return
	}
}

// NewSession creates a new session for next operations
func (engine *Engine) NewSession() *session.Session {
	return session.New(engine.db, &engine.spliter, engine.dbs, engine.operator, engine.config)
}

// TxFunc will be called between tx.Begin() and tx.Commit()
// https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback
type TxFunc func(*session.Session) (interface{}, error)

// Transaction executes sql wrapped in a transaction, then automatically commit if no error occurs
func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) {
	s := engine.NewSession()
	if err := s.Begin(); err != nil {
		return nil, err
	}
	defer func() {
		if p := recover(); p != nil {
			_ = s.Rollback()
			// panic(p) // re-throw panic after Rollback
		} else if err != nil {
			_ = s.Rollback() // err is non-nil; don't change it
		} else {
			err = s.Commit() // err is nil; if Commit returns error update err
		}
	}()

	return f(s)
}
